package jsonprase

import (
	"errors"
	"strconv"
	"strings"
)

type Type int

const (
	JSON_NULL = iota
	JSON_TRUE
	JSON_FALSE
	JSON_NUMBER
	JSON_STRING
	JSON_ARRAY
	JSON_OBJECT
)

type JsonNode struct {
	valuetype Type
	num       float64
	str       string
	arr       []*JsonNode
	obj       map[string]*JsonNode
}

type JsonContext struct {
	data   []byte
	pos    int
	length int
}

func NewJsonPaser(rawjson []byte) *JsonContext {
	return &JsonContext{
		data:   rawjson,
		pos:    0,
		length: len(rawjson),
	}
}

func parsewhite(jc *JsonContext) error {
	for i := jc.pos; i < jc.length; i++ {
		if jc.data[i] == ' ' || jc.data[i] == '\r' || jc.data[i] == '\t' || jc.data[i] == '\n' {
			continue
		}
		jc.pos = i
		return nil
	}
	return nil
}

func parsetrue(jc *JsonContext, jd *JsonNode) error {
	s := "true"

	if jc.length-jc.pos < len(s) {
		return errors.New("PARSE_INVALID_VALUE")
	}
	var i int
	for i = jc.pos; i < jc.pos+len(s) && i < jc.length; i++ {
		if jc.data[i] != s[i-jc.pos] {
			return errors.New("PARSE_INVALID_VALUE")
		}
	}
	jc.pos = i
	jd.valuetype = JSON_TRUE
	return nil
}

func parsefalse(jc *JsonContext, jd *JsonNode) error {
	s := "false"
	if jc.length-jc.pos < len(s) {
		return errors.New("PARSE_INVALID_VALUE")
	}
	var i int
	for i = jc.pos; i < len(s)+jc.pos && i < jc.length; i++ {
		if jc.data[i] != s[i-jc.pos] {
			return errors.New("PARSE_INVALID_VALUE")
		}
	}
	jc.pos = i
	jd.valuetype = JSON_FALSE
	return nil
}

func parsenull(jc *JsonContext, jd *JsonNode) error {
	s := "null"
	if jc.length-jc.pos < len(s) {
		return errors.New("PARSE_INVALID_VALUE")
	}
	var i int
	for i = jc.pos; i < len(s)+jc.pos && i < jc.length; i++ {
		if jc.data[i] != s[i-jc.pos] {
			return errors.New("PARSE_INVALID_VALUE")
		}
	}
	jc.pos = i
	jd.valuetype = JSON_NULL
	return nil
}

func isdigit1to9(ch byte) bool {
	if ch >= '1' && ch <= '9' {
		return true
	} else {
		return false
	}
}

func isdigit0to9(ch byte) bool {
	if ch >= '0' && ch <= '9' {
		return true
	} else {
		return false
	}
}

func parsenumber(jc *JsonContext, jd *JsonNode) error {
	tmp := jc.data
	index := jc.pos
	if tmp[index] == '-' {
		index++
	}
	if tmp[index] == '0' && index < jc.length {
		index++
	} else {
		if index < jc.length && isdigit1to9(tmp[index]) {
			index++
			for index < jc.length {
				if isdigit0to9(tmp[index]) {
					index++
				}
				break
			}
		} else {
			return errors.New("PARSE_INVALID_VALUE")
		}
	}
	if index < jc.length && tmp[index] == '.' {
		index++
		for index < jc.length {
			if !isdigit1to9(tmp[index]) {
				break
			}
			index++
		}
	}
	if index < jc.length && (tmp[index] == 'e' || tmp[index] == 'E') {
		index++
		if index < jc.length && (tmp[index] == '+' || tmp[index] == '-') {
			index++
		}
		for index < jc.length {
			if !isdigit1to9(tmp[index]) {
				break
			}
			index++
		}
	}
	num, err := strconv.ParseFloat(string(jc.data[jc.pos:index]), 64)
	if err != nil {
		return err
	}
	jd.valuetype = JSON_NUMBER
	jd.num = num
	jc.pos = index
	return nil
}

func parsestring(jc *JsonContext, jd *JsonNode) error {
	stack := make([]byte, 0)
	index := jc.pos
	if jc.data[index] == '"' {
		stack = append(stack, jc.data[index])
		index++
		for index < jc.length {
			ch := jc.data[index]
			switch ch {
			case '"':
				index++
				jd.valuetype = JSON_STRING
				jd.str = string(stack[1:])
				jc.pos = index
				return nil
			case '\\':
				index++
				c := jc.data[index]
				switch c {
				case '"':
					stack = append(stack, '"')
					index++
				case '\\':
					stack = append(stack, '\\')
					index++
				case '/':
					stack = append(stack, '/')
					index++
				case 'b':
					stack = append(stack, '\b')
					index++
				case 't':
					stack = append(stack, '\t')
					index++
				case 'n':
					stack = append(stack, '\n')
					index++
				case 'f':
					stack = append(stack, '\f')
					index++
				case 'r':
					stack = append(stack, '\r')
					index++
				case 'u':
				default:
				}
			case 0:
				return errors.New("PARSE_MISS_QUOTATION_MARK")
			default:
				if ch < 0x20 {
					return errors.New("PARSE_INVALID_STRING_CHAR")
				}
				stack = append(stack, ch)
				index++
			}
		}
	}
	return nil
}

func parsearray(jc *JsonContext, jd *JsonNode) error {
	if jc.data[jc.pos] == '[' {
		jc.pos++
		parsewhite(jc)
	}
	jd.arr = make([]*JsonNode, 0)
	stack := make([]*JsonNode, 0)
	if jc.pos < jc.length && jc.data[jc.pos] == ']' {
		jd.valuetype = JSON_ARRAY
		jc.pos++
		return nil
	}

	for {
		e := &JsonNode{
			valuetype: JSON_NULL,
		}
		err := parsevalue(jc, e)
		if err != nil {
			return err
		}
		stack = append(stack, e)
		parsewhite(jc)
		if jc.data[jc.pos] == ',' {
			jc.pos++
			parsewhite(jc)
		} else if jc.data[jc.pos] == ']' {
			jd.valuetype = JSON_ARRAY
			jd.arr = stack
			jc.pos++
			return nil
		} else {
			return errors.New("PARSE_MISS_COMMA_OR_SQUARE_BRACKET")
		}
	}
	return nil
}

func parseobject(jc *JsonContext, jd *JsonNode) error {
	if jc.data[jc.pos] == '{' {
		jc.pos++
		parsewhite(jc)
	}
	jd.obj = make(map[string]*JsonNode)
	if jc.pos < jc.length && jc.data[jc.pos] == '}' {
		jd.valuetype = JSON_OBJECT
		jc.pos++
		return nil
	}

	for {
		key := &JsonNode{
			valuetype: JSON_NULL,
		}
		if jc.data[jc.pos] != '"' {
			return errors.New("PARSE_MISSING_KEY")
		}
		// parse key
		err := parsestring(jc, key)
		if err != nil {
			return err
		}
		parsewhite(jc)

		if jc.data[jc.pos] != ':' {
			return errors.New("PARSE_MISSING_COLON")
		}
		jc.pos++
		parsewhite(jc)

		value := &JsonNode{valuetype: JSON_NULL}
		err = parsevalue(jc, value)
		if err != nil {
			return err
		}
		jd.obj[key.str] = value
		parsewhite(jc)
		if jc.data[jc.pos] == ',' {
			jc.pos++
			parsewhite(jc)
		} else if jc.data[jc.pos] == '}' {
			jd.valuetype = JSON_OBJECT
			//jd.obj.data[key.str] = value
			jc.pos++
			return nil
		} else {
			return errors.New("PARSE_MISS_COMMA_OR_CURLA_BRACKET")
		}
	}

}

func parsevalue(jc *JsonContext, jd *JsonNode) error {
	parsewhite(jc)
	switch jc.data[jc.pos] {
	case 't':
		return parsetrue(jc, jd)
	case 'f':
		return parsefalse(jc, jd)
	case 'n':
		return parsenull(jc, jd)
	case '"':
		return parsestring(jc, jd)
	case '[':
		return parsearray(jc, jd)
	case '{':
		return parseobject(jc, jd)
	case 0:
		return errors.New("PARSE_EXCEPT_VALUE")
	default:
		return parsenumber(jc, jd)
	}
}

func (jc *JsonContext) parse(jd *JsonNode) error {

	parsewhite(jc)
	err := parsevalue(jc, jd)
	if err != nil {
		return err
	}

	parsewhite(jc)

	return nil
}

func (jd *JsonNode) toStr() (string, error) {
	if jd.valuetype == JSON_STRING {
		return jd.str, nil
	} else {
		return "", errors.New("Type isn't Matched!")
	}
}

func (jd *JsonNode) toNum() (float64, error) {
	if jd.valuetype == JSON_NUMBER {
		return jd.num, nil
	} else {
		return 0, errors.New("Type isn't Matched!")
	}
}

func (jd *JsonNode) toArr() ([]*JsonNode, error) {
	if jd.valuetype == JSON_ARRAY {
		return jd.arr, nil
	} else {
		return nil, errors.New("Type isn't Matched!")
	}
}

// get all fields in the JsonNode's obj map
//path example ".Result.Scans[2].ScanID"
func (jd *JsonNode) Get(path string) (*JsonNode, error) {
	patharr := strings.Split(path, ".")
	var tmp *JsonNode
	tmp = jd
	for i := 0; i < len(patharr); i++ {
		if patharr[i] == "" {
			continue
		}
		if strings.ContainsAny(patharr[i], "[]") {
			index, err := strconv.Atoi(string(patharr[i][len(patharr[i])-2]))
			if err != nil {
				return nil, err
			}
			field := patharr[i][0 : len(patharr[i])-3]
			if _, ok := tmp.obj[field]; !ok {
				return nil, errors.New(patharr[i] + " field is not exists")
			}
			//fmt.Println(patharr[i][0 : len(patharr[i])-3])
			tmparr := tmp.obj[field].arr
			if index > len(tmparr)-1 {
				return nil, errors.New("index out of arr's length")
			} else {
				tmp = tmparr[index]
			}
		} else {
			if _, ok := tmp.obj[patharr[i]]; !ok {
				return nil, errors.New(patharr[i] + " field is not exists")
			}
			tmp = tmp.obj[patharr[i]]
		}

	}
	return tmp, nil
}

func NewJsonObj() *JsonNode {
	return &JsonNode{
		valuetype: JSON_OBJECT,
		obj:       make(map[string]*JsonNode),
	}
}

func (jd *JsonNode) AddStringToObj(key, value string) {
	jd.obj[key] = &JsonNode{valuetype: JSON_STRING, str: value}
}

func (jd *JsonNode) AddNumberToObj(key string, value float64) {
	jd.obj[key] = &JsonNode{valuetype: JSON_NUMBER, num: value}
}

func (jd *JsonNode) AddArrayToObj(key string, value []*JsonNode) {
	jd.obj[key] = &JsonNode{valuetype: JSON_ARRAY, arr: value}
}

func (jd *JsonNode) AddObjectToObj(key string, value map[string]*JsonNode) {
	jd.obj[key] = &JsonNode{valuetype: JSON_OBJECT, obj: value}
}

func (jd *JsonNode) AddNullToObj(key string) {
	jd.obj[key] = &JsonNode{valuetype: JSON_NULL}
}

func (jd *JsonNode) AddTrueToObj(key string) {
	jd.obj[key] = &JsonNode{valuetype: JSON_TRUE}
}

func (jd *JsonNode) AddFalseToObj(key string) {
	jd.obj[key] = &JsonNode{valuetype: JSON_FALSE}
}

func addstrtoslice(s string, slice *[]byte) {
	*slice = append(*slice, []byte(s)...)
}

func stringfystring(key string, jc *JsonContext) {
	addstrtoslice("\"", &jc.data)
	for i := 0; i < len(key); i++ {
		switch key[i] {
		case '"':
			addstrtoslice("\\\"", &jc.data)
		case '\\':
			addstrtoslice("\\\\", &jc.data)
		case '\b':
			addstrtoslice("\\b", &jc.data)
		case '\f':
			addstrtoslice("\\f", &jc.data)
		case '\n':
			addstrtoslice("\\f", &jc.data)
		case '\r':
			addstrtoslice("\\r", &jc.data)
		case '\t':
			addstrtoslice("\\t", &jc.data)
		default:
			addstrtoslice(string(key[i]), &jc.data)
		}
	}
	addstrtoslice("\"", &jc.data)
}

func objtostring(jd *JsonNode,jc *JsonContext) string {

	switch jd.valuetype {
	case JSON_NULL:
		addstrtoslice("null", &jc.data)
	case JSON_TRUE:
		addstrtoslice("true", &jc.data)
	case JSON_FALSE:
		addstrtoslice("false", &jc.data)
	case JSON_NUMBER:
		addstrtoslice(strconv.FormatFloat(jd.num, 'g', 30, 64), &jc.data)
	case JSON_STRING:
		stringfystring(jd.str, jc)
	case JSON_ARRAY:
		addstrtoslice("[", &jc.data)
		for i := 0; i < len(jd.arr); i++ {
			objtostring(jd.arr[i],jc)
			if i == len(jd.arr)-1 {
				break
			}
			addstrtoslice(",", &jc.data)
		}
		addstrtoslice("]", &jc.data)
	case JSON_OBJECT:
		addstrtoslice("{", &jc.data)
		counter := 0
		for k, v := range jd.obj {
			counter++
			stringfystring(k, jc)
			addstrtoslice(":", &jc.data)
			objtostring(v,jc)
			if counter == len(jd.obj) {
				break
			}
			addstrtoslice(",", &jc.data)
		}
		addstrtoslice("}", &jc.data)
	}

	return string(jc.data)
}

func (jd *JsonNode) ObjToString() string {
	jc := &JsonContext{data: make([]byte, 0)}
	return objtostring(jd,jc)
}
